import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
from pathlib import Path
from great_tables import GT
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, roc_curve,
confusion_matrix, classification_report)Анализ данных о сердечно-сосудистых заболеваниях (поиск инсайтов, составление рекомендаций стейкхолдерам)
В данном исследовании проводится комплексный анализ данных о сердечно-сосудистых заболеваниях с целью выявления ключевых факторов риска и построения предиктивных моделей. Анализ включает исследовательский анализ данных, разработку и сравнение моделей машинного обучения для прогнозирования наличия сердечно-сосудистых заболеваний.
Введение
Сердечно-сосудистые заболевания являются основной причиной смертности во многих странах мира. Раннее выявление факторов риска и своевременная профилактика играют ключевую роль в снижении заболеваемости и смертности.
Цель исследования
Основной целью данного исследования является анализ факторов риска сердечно-сосудистых заболеваний на основе данных медицинских обследований и построение предиктивных моделей для оценки вероятности наличия заболевания.
Задачи исследования
- Провести исследовательский анализ данных для выявления ключевых закономерностей
- Выполнить очистку и предобработку данных
- Построить и оценить предиктивные модели
- Сформулировать практические рекомендации для заинтересованных лиц
Основные стейкхолдеры
1. Медицинская лаборатория
Приоритеты:
- Повышение точности диагностики сердечно-сосудистых заболеваний
- Оптимизация скрининговых программ
- Снижение затрат на обработку данных
- Улучшение качества предоставляемых услуг
Задачи:
- Внедрение предиктивных моделей в рутинную практику
- Обучение персонала работе с ML-инструментами
- Интеграция моделей в существующие лабораторные системы
- Мониторинг эффективности внедренных решений
2. Врачи-кардиологи и терапевты
Приоритеты:
- Получение точных инструментов для оценки риска пациентов
- Сокращение времени на принятие клинических решений
- Повышение качества лечения и профилактики
- Снижение пропускной способности высокорисковых пациентов
Задачи:
- Использование предиктивных моделей в клинической практике
- Интерпретация результатов ML-моделей для пациентов
- Адапация рекомендаций под индивидуальные особенности пациентов
- Обеспечение этического использования алгоритмов
3. Пациенты
Приоритеты:
- Своевременное выявление рисков сердечно-сосудистых заболеваний
- Получение персонализированных рекомендаций
- Повышение качества жизни и здоровья
- Снижение тревожности относительно состояния здоровья
Задачи:
- Прохождение регулярных обследований
- Следование рекомендациям по изменению образа жизни
- Активное участие в программах мониторинга здоровья
- Соблюдение предписанного лечения
4. Система здравоохранения
Приоритеты:
- Снижение общей заболеваемости и смертности от ССЗ
- Оптимизация распределения медицинских ресурсов
- Повышение эффективности профилактических программ
- Снижение экономических затрат на лечение ССЗ
Задачи:
- Разработка и внедрение национальных скрининговых программ
- Создание реестров пациентов с высоким риском
- Обеспечение доступности качественной медицинской помощи
- Мониторинг популяционных показателей здоровья
5. Страховые компании
Приоритеты:
- Снижение выплат по дорогостоящим случаям лечения ССЗ
- Оптимизация тарифов страховых продуктов
- Повышение удержания клиентов через профилактические программы
- Точный расчет актуарных рисков
Задачи:
- Разработка программ превентивной медицины
- Интеграция моделей оценки рисков в андеррайтинг
- Создание стимулов для здорового образа жизни клиентов
- Мониторинг медицинских расходов клиентов
6. Исследователи и академическое сообщество
Приоритеты:
- Получение новых научных знаний о факторах риска ССЗ
- Валидация методологий машинного обучения в медицине
- Публикация результатов в рецензируемых журналах
- Развитие междисциплинарного сотрудничества
Задачи:
- Проведение дополнительных исследований на расширенных данных
- Валидация моделей на независимых выборках
- Разработка новых методологий анализа
- Подготовка научных публикаций и презентаций
7. Разработчики медицинских технологий
Приоритеты:
- Создание коммерчески жизнеспособных продуктов
- Обеспечение соответствия регуляторным требованиям
- Масштабирование решений для широкого использования
- Поддержание конкурентоспособности на рынке
Задачи:
- Разработка пользовательских интерфейсов для клиницистов
- Интеграция с существующими медицинскими системами (HIS/EMR)
- Обеспечение безопасности и конфиденциальности данных
- Проведение клинических испытаний и сертификация
Ключевые метрики успеха для стейкхолдеров
Медицинская лаборатория:
- Снижение времени обработки анализов
- Повышение точности прогнозирования
- Увеличение количества обслуживаемых пациентов
Врачи:
- Сокращение времени на принятие решений
- Повышение выявляемости заболеваний на ранних стадиях
- Удовлетворенность пациентов
Пациенты:
- Повышение приверженности лечению
- Снижение прогрессирования заболеваний
- Улучшение качества жизни
Система здравоохранения:
- Снижение госпитализаций по поводу ССЗ
- Экономическая эффективность
- Покрытие скринингом большей части целевой популяции
Обзор данных
В исследовании используется датасет Cardiovascular Disease Dataset, содержащий информацию о 70 000 пациентах. Данные предоставлены медицинской лабораторией и включают 11 признаков и целевую переменную наличия сердечно-сосудистого заболевания.
Описание признаков
- age - возраст в днях
- gender - пол (1 - женщина, 2 - мужчина)
- height - рост в см
- weight - вес в кг
- ap_hi - систолическое артериальное давление
- ap_lo - диастолическое артериальное давление
- cholesterol - уровень холестерина (1: нормальный, 2: выше нормы, 3: высокий)
- gluc - уровень глюкозы (1: нормальный, 2: выше нормы, 3: высокий)
- smoke - курение (0: нет, 1: да)
- alco - употребление алкоголя (0: нет, 1: да)
- active - физическая активность (0: нет, 1: да)
- cardio - наличие сердечно-сосудистого заболевания (0: нет, 1: да)
Методология
Подходы к анализу
Исследование будет проводиться в несколько этапов:
- Исследовательский анализ данных (EDA): анализ распределений, выявление выбросов, изучение взаимосвязей
- Предобработка данных: очистка, нормализация, создание новых признаков
- Моделирование: построение и сравнение моделей машинного обучения
- Интерпретация результатов: анализ важности признаков и формулирование выводов
Инструменты анализа
- Python 3.12+ с научными библиотеками pandas, numpy, matplotlib, seaborn
- scikit-learn для построения моделей машинного обучения
- Quarto для генерации отчета
Результаты EDA
Настройка окружения
Для начала импортируем необходимые библиотеки и настроим параметры визуализации.
Настроим параметры отображения в ноутбуке
np.random.seed(31337)
warnings.filterwarnings('ignore')
# Настройки для визуализаций
plt.style.use('seaborn-v0_8-whitegrid')
# Монохромная палитра с красными акцентами
colors = ['#808080', '#606060', '#404040', '#FF6B6B', '#CC5555']
sns.set_palette(colors)
plt.rcParams['font.size'] = 11
plt.rcParams['figure.titlesize'] = 14
plt.rcParams['axes.titlesize'] = 12
plt.rcParams['axes.labelsize'] = 11Вывод: Окружение настроено, необходимые библиотеки импортированы, параметры визуализации заданы.
Загрузка данных
Загрузим набор данных и выведем основную информацию о его размере.
DF_CSV_PATH = 'data/cardio_train.csv'
df = pd.read_csv(DF_CSV_PATH, sep=';')Вывод: Данные успешно загружены. Исходный файл прочитан корректно.
Предварительный просмотр
Ознакомимся со структурой данных предоставленного датасета.
print(f"Размер датасета: {df.shape}")Размер датасета: (70000, 13)
Вывод: Исходный набор данных содержит 70 000 записей и 13 столбцов.
GT(df.head())| id | age | gender | height | weight | ap_hi | ap_lo | cholesterol | gluc | smoke | alco | active | cardio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 18393 | 2 | 168 | 62.0 | 110 | 80 | 1 | 1 | 0 | 0 | 1 | 0 |
| 1 | 20228 | 1 | 156 | 85.0 | 140 | 90 | 3 | 1 | 0 | 0 | 1 | 1 |
| 2 | 18857 | 1 | 165 | 64.0 | 130 | 70 | 3 | 1 | 0 | 0 | 0 | 1 |
| 3 | 17623 | 2 | 169 | 82.0 | 150 | 100 | 1 | 1 | 0 | 0 | 1 | 1 |
| 4 | 17474 | 1 | 156 | 56.0 | 100 | 60 | 1 | 1 | 0 | 0 | 0 | 0 |
Вывод: Структура данных соответствует описанию: присутствуют ID, возраст, пол, антропометрические данные и показатели здоровья.
Типы данных
Проверим типы данных каждого признака, чтобы убедиться в их корректности.
types_df = df.dtypes.reset_index()
types_df.columns = ["Признак", "Тип данных"]
GT(types_df)| Признак | Тип данных |
|---|---|
| id | int64 |
| age | int64 |
| gender | int64 |
| height | int64 |
| weight | float64 |
| ap_hi | int64 |
| ap_lo | int64 |
| cholesterol | int64 |
| gluc | int64 |
| smoke | int64 |
| alco | int64 |
| active | int64 |
| cardio | int64 |
Вывод: Типы данных интерпретированы корректно (целые и вещественные числа), дополнительных преобразований типов на данном этапе не требуется.
Описательная статистика
Рассмотрим основные статистические характеристики числовых признаков.
stats_df = df.describe().reset_index()
GT(stats_df)| index | id | age | gender | height | weight | ap_hi | ap_lo | cholesterol | gluc | smoke | alco | active | cardio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 | 70000.0 |
| mean | 49972.4199 | 19468.865814285713 | 1.3495714285714286 | 164.35922857142856 | 74.20569 | 128.8172857142857 | 96.63041428571428 | 1.3668714285714285 | 1.226457142857143 | 0.08812857142857143 | 0.053771428571428574 | 0.8037285714285715 | 0.4997 |
| std | 28851.30232317292 | 2467.2516672414013 | 0.47683801558286387 | 8.210126364538038 | 14.395756678511379 | 154.01141945609137 | 188.47253029639026 | 0.680250348699381 | 0.572270276613845 | 0.28348381676993517 | 0.2255677036041049 | 0.3971790635049283 | 0.5000034814661862 |
| min | 0.0 | 10798.0 | 1.0 | 55.0 | 10.0 | -150.0 | -70.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 25% | 25006.75 | 17664.0 | 1.0 | 159.0 | 65.0 | 120.0 | 80.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 50% | 50001.5 | 19703.0 | 1.0 | 165.0 | 72.0 | 120.0 | 80.0 | 1.0 | 1.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 75% | 74889.25 | 21327.0 | 2.0 | 170.0 | 82.0 | 140.0 | 90.0 | 2.0 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 |
| max | 99999.0 | 23713.0 | 2.0 | 250.0 | 200.0 | 16020.0 | 11000.0 | 3.0 | 3.0 | 1.0 | 1.0 | 1.0 | 1.0 |
Вывод: Описательная статистика указывает на наличие аномальных значений (выбросов) в полях роста, веса и артериального давления, которые потребуют очистки.
Проверка на пропуски
Важным этапом является проверка данных на наличие пропущенных значений.
# Проверка пропусков
missing_values = df.isnull().sum().reset_index()
missing_values.columns = ["Признак", "Количество пропусков"]
if missing_values["Количество пропусков"].sum() == 0:
print("Пропусков не обнаружено")
else:
GT(missing_values[missing_values["Количество пропусков"] > 0])Пропусков не обнаружено
Вывод: Пропущенные значения в датасете не обнаружены.
Проверка дубликатов
Проверим наличие полных дубликатов записей, которые могут исказить результаты анализа, и удалим их при наличии.
# Проверка дубликатов
duplicates = df.duplicated().sum()
print(f"Количество полных дубликатов: {duplicates}")
# Удаление дубликатов если есть
if duplicates > 0:
df = df.drop_duplicates()
print(f"После удаления дубликатов размер: {df.shape}")Количество полных дубликатов: 0
Вывод: Проверка на дубликаты выполнена. Уникальность записей подтверждена (или восстановлена).
Анализ выбросов
Используем диаграммы размаха (boxplot) для выявления аномальных значений в числовых признаках.
Для удобства анализа преобразуем возраст из дней в годы во временной колонке для визуализации (до очистки).
df['age_years_raw'] = df['age'] / 365.25Выбросы: Возраст
plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='age_years_raw', color='#808080')
plt.title('Box Plot: Возраст', fontsize=14)
plt.xlabel('Лет')
plt.show()Вывод: Распределение возраста не содержит явных аномалий, диапазон значений реалистичен.
Выбросы: Рост
plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='height', color='#808080')
plt.title('Box Plot: Рост', fontsize=14)
plt.xlabel('см')
plt.show()Вывод: Присутствуют выбросы в росте (слишком низкие и высокие значения), которые вероятно являются ошибками ввода.
Выбросы: Вес
plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='weight', color='#808080')
plt.title('Box Plot: Вес', fontsize=14)
plt.xlabel('кг')
plt.show()Вывод: Аналогично росту, вес содержит подозрительные экстремальные значения.
Выбросы: Систолическое давление
plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='ap_hi', color='#808080')
plt.title('Box Plot: Систолическое давление', fontsize=14)
plt.xlabel('мм рт.ст.')
plt.show()Вывод: Данные по давлению сильно “зашумлены” экстремальными выбросами, что подтверждает необходимость жесткой фильтрации.
Выбросы: Диастолическое давление
plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='ap_lo', color='#808080')
plt.title('Box Plot: Диастолическое давление', fontsize=14)
plt.xlabel('мм рт.ст.')
plt.show()Вывод: Диастолическое давление также требует очистки от нереалистичных значений.
Количественная оценка выбросов по методу межквартильного размаха (IQR).
numeric_features = ['age_years_raw', 'height', 'weight', 'ap_hi', 'ap_lo']
outliers_data = []
for feature in numeric_features:
Q1 = df[feature].quantile(0.25)
Q3 = df[feature].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_count = len(df[(df[feature] < lower_bound) | (df[feature] > upper_bound)])
outliers_data.append({
'Признак': feature,
'Количество выбросов': outliers_count,
'Процент': f"{outliers_count/len(df)*100:.1f}%"
})
GT(pd.DataFrame(outliers_data))| Признак | Количество выбросов | Процент |
|---|---|---|
| age_years_raw | 4 | 0.0% |
| height | 519 | 0.7% |
| weight | 1819 | 2.6% |
| ap_hi | 1435 | 2.1% |
| ap_lo | 4632 | 6.6% |
Вывод: Статистика IQR подтверждает, что наибольшее количество выбросов содержится в показателях давления, что критично для корректного моделирования.
Очистка данных
На основе EDA проведем очистку данных от аномальных и нереалистичных значений.
Инициализация
Создадим копию датафрейма для очистки.
df_clean = df.copy()
# Удаляем временную колонку, если она осталась
if 'age_years_raw' in df_clean.columns:
df_clean = df_clean.drop('age_years_raw', axis=1)
print(f"Исходный размер датасета: {df_clean.shape}")Исходный размер датасета: (70000, 13)
Вывод: Подготовка к очистке выполнена, работаем с копией данных для безопасности.
Очистка артериального давления
Фильтрация нереалистичных значений давления. Используем следующие критерии: - Систолическое: 70-250 мм рт.ст. - Диастолическое: 40-150 мм рт.ст. - Систолическое должно быть выше диастолического.
before_pressure = len(df_clean)
df_clean = df_clean[
(df_clean['ap_hi'] >= 70) & (df_clean['ap_hi'] <= 250) &
(df_clean['ap_lo'] >= 40) & (df_clean['ap_lo'] <= 150) &
(df_clean['ap_hi'] > df_clean['ap_lo'])
]
after_pressure = len(df_clean)
print(f"Удалено записей с нереалистичным давлением: {before_pressure - after_pressure}")Удалено записей с нереалистичным давлением: 1334
Вывод: Фильтрация давления удалила наиболее грубые ошибки, существенно повысив качество данных.
Очистка антропометрических данных
Фильтрация по росту и весу: - Рост: 100-220 см - Вес: 30-250 кг
before_anthro = len(df_clean)
df_clean = df_clean[
(df_clean['height'] >= 100) & (df_clean['height'] <= 220) &
(df_clean['weight'] >= 30) & (df_clean['weight'] <= 250)
]
after_anthro = len(df_clean)
print(f"Удалено записей с нереалистичным ростом/весом: {before_anthro - after_anthro}")Удалено записей с нереалистичным ростом/весом: 33
Вывод: Исключены записи с физиологически невозможными сочетаниями роста и веса.
Очистка и преобразование возраста
Оставляем пациентов от 18 до 100 лет и создаем признак age_years для анализа.
# Создаем возраст в годах
df_clean['age_years'] = df_clean['age'] / 365.25
before_age = len(df_clean)
df_clean = df_clean[
(df_clean['age_years'] >= 18) & (df_clean['age_years'] <= 100)
]
after_age = len(df_clean)
print(f"Удалено записей с нереалистичным возрастом: {before_age - after_age}")Удалено записей с нереалистичным возрастом: 0
Вывод: Возрастной фильтр отработал, данные по возрасту были достаточно чистыми. Возраст успешно сконвертирован в годы.
Расчет BMI
Рассчитаем индекс массы тела (BMI) для дальнейшего анализа.
df_clean['bmi'] = df_clean['weight'] / (df_clean['height'] / 100) ** 2
print(f"Итоговый размер после очистки: {df_clean.shape}")Итоговый размер после очистки: (68633, 15)
Вывод: Рассчитан BMI, который является интегральным показателем, часто более информативным, чем вес и рост по отдельности.
Статистика очищенного датасета:
clean_stats = df_clean[['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']].describe().reset_index()
GT(clean_stats)| index | age_years | height | weight | ap_hi | ap_lo | bmi |
|---|---|---|---|---|---|---|
| count | 68633.0 | 68633.0 | 68633.0 | 68633.0 | 68633.0 | 68633.0 |
| mean | 53.291214957737346 | 164.3946206635292 | 74.11911034050677 | 126.67120772806085 | 81.30172074657963 | 27.473124357736904 |
| std | 6.757253833720525 | 7.977184812426184 | 14.307359581664704 | 16.681362962533587 | 9.422616258222744 | 5.351510180908495 |
| min | 29.56331279945243 | 100.0 | 30.0 | 70.0 | 40.0 | 10.726643598615919 |
| 25% | 48.34496919917864 | 159.0 | 65.0 | 120.0 | 80.0 | 23.875114784205696 |
| 50% | 53.93839835728953 | 165.0 | 72.0 | 120.0 | 80.0 | 26.346494034400994 |
| 75% | 58.38193018480493 | 170.0 | 82.0 | 140.0 | 90.0 | 30.119375573921033 |
| max | 64.92265571526352 | 207.0 | 200.0 | 240.0 | 150.0 | 152.55177514792896 |
Вывод: После очистки статистики (min/max/std) выглядят правдоподобно и пригодны для анализа.
Анализ категориальных признаков (структура)
Посмотрим на уникальные значения в категориальных переменных для понимания их структуры (на очищенных данных).
categorical_cols = ['gender', 'cholesterol', 'gluc', 'smoke', 'alco', 'active', 'cardio']
unique_data = []
for col in categorical_cols:
unique_vals = sorted(df_clean[col].unique())
unique_data.append({"Признак": col, "Уникальные значения": str(unique_vals)})
GT(pd.DataFrame(unique_data))| Признак | Уникальные значения |
|---|---|
| gender | [np.int64(1), np.int64(2)] |
| cholesterol | [np.int64(1), np.int64(2), np.int64(3)] |
| gluc | [np.int64(1), np.int64(2), np.int64(3)] |
| smoke | [np.int64(0), np.int64(1)] |
| alco | [np.int64(0), np.int64(1)] |
| active | [np.int64(0), np.int64(1)] |
| cardio | [np.int64(0), np.int64(1)] |
Вывод: Значения категориальных признаков соответствуют ожидаемым и не содержат неявных дубликатов или ошибок ввода.
Распределение целевой переменной
Проанализируем сбалансированность классов целевой переменной cardio в очищенном датасете.
plt.figure(figsize=(8, 6))
ax = sns.countplot(data=df_clean, x='cardio', palette=['#808080', '#FF6B6B'])
plt.title('Распределение наличия сердечно-сосудистых заболеваний', fontsize=14, pad=20)
plt.xlabel('Наличие заболевания (0 - нет, 1 - да)', fontsize=12)
plt.ylabel('Количество пациентов', fontsize=12)
# Добавление процентов
total = len(df_clean)
for p in ax.patches:
percentage = f'{100 * p.get_height() / total:.1f}%'
ax.annotate(percentage, (p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom', fontsize=11)
plt.tight_layout()
plt.show()Детальная статистика распределения целевой переменной:
target_stats = df_clean['cardio'].value_counts().reset_index()
target_stats.columns = ['Cardio', 'Count']
target_stats['Percentage'] = (target_stats['Count'] / total * 100).round(1).astype(str) + '%'
GT(target_stats)| Cardio | Count | Percentage |
|---|---|---|
| 0 | 34679 | 50.5% |
| 1 | 33954 | 49.5% |
Вывод: Классы сбалансированы (~50% на 50%), что позволяет использовать accuracy как одну из метрик и не требует применения техник балансировки (SMOTE и др.).
Распределения числовых признаков (очищенные данные)
Возраст
Рассмотрим распределение возраста пациентов.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='age_years', bins=30, color='#808080', alpha=0.7)
plt.title('Распределение возраста (годы)', fontsize=14)
plt.xlabel('Возраст, лет')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()Вывод: Основная масса пациентов находится в возрасте от 40 до 65 лет, что соответствует группе риска ССЗ.
Рост
Анализ распределения роста пациентов.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='height', bins=30, color='#808080', alpha=0.7)
plt.title('Распределение роста', fontsize=14)
plt.xlabel('Рост, см')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()Вывод: Распределение роста близко к нормальному.
Вес
Анализ распределения веса пациентов.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='weight', bins=30, color='#808080', alpha=0.7)
plt.title('Распределение веса', fontsize=14)
plt.xlabel('Вес, кг')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()Вывод: Распределение веса имеет “тяжелый” правый хвост, указывающий на наличие пациентов с значительным избыточным весом.
Систолическое давление
Распределение верхнего (систолического) артериального давления.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='ap_hi', bins=30, color='#808080', alpha=0.7)
plt.title('Систолическое артериальное давление', fontsize=14)
plt.xlabel('Давление, мм рт.ст.')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()Вывод: Данные по давлению теперь находятся в реалистичном диапазоне.
Диастолическое давление
Распределение нижнего (диастолического) артериального давления.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='ap_lo', bins=30, color='#808080', alpha=0.7)
plt.title('Диастолическое артериальное давление', fontsize=14)
plt.xlabel('Давление, мм рт.ст.')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()Вывод: Данные диастолического давления очищены.
Распределения категориальных признаков
Проанализируем категориальные факторы риска на очищенных данных.
Пол
Соотношение мужчин и женщин в выборке.
gender_counts = df_clean['gender'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Женщины', 'Мужчины'], y=gender_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Распределение по полу', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Женщины составляют большую часть выборки (~65%), что необходимо учитывать при интерпретации результатов.
Холестерин
Уровни холестерина среди пациентов.
cholesterol_counts = df_clean['cholesterol'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
plt.bar(['Норма', 'Выше нормы', 'Высокий'], cholesterol_counts.values,
color=['#808080', '#606060', '#FF6B6B'])
plt.title('Уровень холестерина', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Большинство пациентов имеют нормальный уровень холестерина, но значительная доля (около 25%) находится в зоне повышенного риска.
Глюкоза
Уровни глюкозы среди пациентов.
gluc_counts = df_clean['gluc'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
plt.bar(['Норма', 'Выше нормы', 'Высокий'], gluc_counts.values,
color=['#808080', '#606060', '#FF6B6B'])
plt.title('Уровень глюкозы', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Аналогично холестерину, повышенный уровень глюкозы наблюдается у меньшинства, однако это важный фактор риска.
Курение
Доля курящих пациентов.
smoke_counts = df_clean['smoke'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Не курят', 'Курят'], y=smoke_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Курение', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Курящие пациенты составляют меньшую часть выборки. Интересно проверить корреляцию курения с полом и ССЗ.
Алкоголь
Доля пациентов, употребляющих алкоголь.
alco_counts = df_clean['alco'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Не употребляют', 'Употребляют'], y=alco_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Употребление алкоголя', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Употребление алкоголя задекларировано лишь у малой части пациентов (около 5%), что может быть связано с особенностями сбора данных (социальная желательность).
Физическая активность
Уровень физической активности пациентов.
active_counts = df_clean['active'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Неактивны', 'Активны'], y=active_counts.values,
palette=['#808080', '#FF6B6B'])
plt.title('Физическая активность', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Большинство пациентов (около 80%) отмечают наличие физической активности.
Анализ BMI и категоризация
Распределение BMI
Посмотрим на распределение индекса массы тела в очищенной выборке.
plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='bmi', bins=30, color='#808080', alpha=0.7)
plt.axvline(x=18.5, color='blue', linestyle='--', alpha=0.7, label='Недостаточный вес')
plt.axvline(x=25, color='green', linestyle='--', alpha=0.7, label='Норма')
plt.axvline(x=30, color='orange', linestyle='--', alpha=0.7, label='Избыточный вес')
plt.axvline(x=35, color='red', linestyle='--', alpha=0.7, label='Ожирение')
plt.title('Распределение BMI', fontsize=14)
plt.xlabel('BMI')
plt.ylabel('Частота')
plt.legend()
plt.show()Вывод: Распределение BMI смещено вправо, значительная часть популяции имеет избыточный вес.
Категории BMI
Разделим пациентов на группы согласно классификации ВОЗ.
def categorize_bmi(bmi):
if bmi < 18.5:
return 'Недостаточный вес'
elif bmi < 25:
return 'Норма'
elif bmi < 30:
return 'Избыточный вес'
elif bmi < 35:
return 'Ожирение I степени'
else:
return 'Ожирение II+ степени'
df_clean['bmi_category'] = df_clean['bmi'].apply(categorize_bmi)
bmi_counts = df_clean['bmi_category'].value_counts()Вывод: Категоризация выполнена успешно. Это позволит проанализировать риски для разных групп по весу.
Визуализация распределения по категориям:
colors_bmi = ['#404040', '#606060', '#808080', '#FF6B6B', '#CC5555']
plt.figure(figsize=(10, 6))
sns.barplot(x=bmi_counts.index, y=bmi_counts.values, palette=colors_bmi)
plt.title('Категории BMI', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()Вывод: Визуализация подтверждает, что нормальный вес имеет лишь меньшая часть обследованных. Группы риска (избыточный вес и ожирение) доминируют.
Детальная статистика по категориям BMI:
bmi_table = bmi_counts.reset_index()
bmi_table.columns = ['Категория', 'Количество']
bmi_table['Доля'] = (bmi_table['Количество'] / len(df_clean) * 100).round(1).astype(str) + '%'
GT(bmi_table)| Категория | Количество | Доля |
|---|---|---|
| Норма | 25424 | 37.0% |
| Избыточный вес | 24620 | 35.9% |
| Ожирение I степени | 11938 | 17.4% |
| Ожирение II+ степени | 6015 | 8.8% |
| Недостаточный вес | 636 | 0.9% |
Вывод: Более 60% пациентов имеют вес выше нормы, что является серьезным фактором риска для сердечно-сосудистой системы.
Корреляционный анализ
Изучим линейные взаимосвязи между признаками на очищенных данных, построив матрицу корреляций.
# Подготовка данных для корреляции (исключаем ID и категориальную переменную BMI)
df_corr = df_clean.drop(['id', 'bmi_category'], axis=1)
# Расчет корреляционной матрицы
correlation_matrix = df_corr.corr()
# Создание маски для верхней треугольной части
mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, mask=mask, annot=True, cmap='RdYlBu_r', center=0,
square=True, fmt='.2f', cbar_kws={"shrink": .8})
plt.title('Корреляционная матрица признаков', fontsize=16, pad=20)
plt.tight_layout()
plt.show()Вывод: Корреляционная матрица не выявила мультиколлинеарности, но показала заметную связь между давлением и целевой переменной.
Выделим наиболее сильные корреляции для детального рассмотрения.
strong_correlations = []
for i in range(len(correlation_matrix.columns)):
for j in range(i):
if abs(correlation_matrix.iloc[i, j]) > 0.3:
strong_correlations.append({
'Пара признаков': f"{correlation_matrix.columns[i]} - {correlation_matrix.columns[j]}",
'Коэффициент корреляции': correlation_matrix.iloc[i, j]
})
GT(pd.DataFrame(strong_correlations))| Пара признаков | Коэффициент корреляции |
|---|---|
| height - gender | 0.5123460444729824 |
| weight - height | 0.30181460611672883 |
| ap_lo - ap_hi | 0.734688808991578 |
| gluc - cholesterol | 0.4505529217841897 |
| smoke - gender | 0.33889418419569894 |
| alco - smoke | 0.34033647396206457 |
| cardio - ap_hi | 0.42814741718728827 |
| cardio - ap_lo | 0.34074195525362994 |
| age_years - age | 0.9999999999999921 |
| bmi - weight | 0.8511165932301601 |
Вывод: Сильнейшие корреляции наблюдаются между систолическим и диастолическим давлением, а также между ростом и полом.
Построение моделей
Подготовка данных для моделирования
Разделение данных на матрицу признаков (X) и целевой вектор (y).
# Удаляем нерелевантные признаки и подготовляем X, y
X = df_clean.drop(['id', 'age', 'cardio', 'bmi_category'], axis=1)
y = df_clean['cardio']
print(f"Признаки для моделирования: {list(X.columns)}")
print(f"Размер признакового пространства: {X.shape}")Признаки для моделирования: ['gender', 'height', 'weight', 'ap_hi', 'ap_lo', 'cholesterol', 'gluc', 'smoke', 'alco', 'active', 'age_years', 'bmi']
Размер признакового пространства: (68633, 12)
Вывод: Данные подготовлены: целевая переменная выделена, удалены вспомогательные столбцы (ID, возраст в днях). Осталось 11 предикторов.
Разделение на обучающую и тестовую выборки.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Размер обучающей выборки: {X_train.shape}")
print(f"Размер тестовой выборки: {X_test.shape}")Размер обучающей выборки: (54906, 12)
Размер тестовой выборки: (13727, 12)
Вывод: Выборка успешно разделена на Train/Test (80/20) с сохранением баланса классов (stratify).
Стандартизация числовых признаков для улучшения работы линейных моделей.
numeric_features = ['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']
scaler = StandardScaler()
X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()
X_train_scaled[numeric_features] = scaler.fit_transform(X_train[numeric_features])
X_test_scaled[numeric_features] = scaler.transform(X_test[numeric_features])
print("Числовые признаки стандартизированы")Числовые признаки стандартизированы
Вывод: StandardScaler применен. Это критически важно для логистической регрессии, чтобы веса признаков были сопоставимы.
Обучение моделей
Настройка кросс-валидации.
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)Logistic Regression
Обучение логистической регрессии как базовой модели.
print("Обучение Logistic Regression...")
lr_model = LogisticRegression(random_state=42, max_iter=1000)
# Cross-validation (accuracy)
lr_cv_scores = cross_val_score(lr_model, X_train_scaled, y_train, cv=cv, scoring='accuracy')
print(f"Logistic Regression CV Accuracy: {lr_cv_scores.mean():.4f} ± {lr_cv_scores.std():.4f}")
# Обучение на полных данных
lr_model.fit(X_train_scaled, y_train)
# Оценка Accuracy на Train и Test
lr_train_acc = accuracy_score(y_train, lr_model.predict(X_train_scaled))
lr_test_acc = accuracy_score(y_test, lr_model.predict(X_test_scaled))
print(f"LR Train Accuracy: {lr_train_acc:.4f}")
print(f"LR Test Accuracy: {lr_test_acc:.4f}")Обучение Logistic Regression...
Logistic Regression CV Accuracy: 0.7271 ± 0.0033
LR Train Accuracy: 0.7273
LR Test Accuracy: 0.7275
Вывод: Модель логистической регрессии обучена.
Random Forest
Обучение случайного леса для выявления нелинейных зависимостей.
print("Обучение Random Forest...")
rf_model = RandomForestClassifier(n_estimators=100, random_state=42, max_depth=10)
# Cross-validation (accuracy)
rf_cv_scores = cross_val_score(rf_model, X_train, y_train, cv=cv, scoring='accuracy')
print(f"Random Forest CV Accuracy: {rf_cv_scores.mean():.4f} ± {rf_cv_scores.std():.4f}")
# Обучение на полных данных
rf_model.fit(X_train, y_train)
# Оценка Accuracy на Train и Test
rf_train_acc = accuracy_score(y_train, rf_model.predict(X_train))
rf_test_acc = accuracy_score(y_test, rf_model.predict(X_test))
print(f"RF Train Accuracy: {rf_train_acc:.4f}")
print(f"RF Test Accuracy: {rf_test_acc:.4f}")Обучение Random Forest...
Random Forest CV Accuracy: 0.7334 ± 0.0030
RF Train Accuracy: 0.7542
RF Test Accuracy: 0.7355
Вывод: Random Forest обучен.
Оценка качества моделей
Определим функцию для расчета метрик.
def evaluate_model(model, X_test_data, y_test_data, model_name):
# Предсказания
y_pred = model.predict(X_test_data)
y_pred_proba = model.predict_proba(X_test_data)[:, 1]
# Метрики
cm = confusion_matrix(y_test_data, y_pred)
tn, fp, fn, tp = cm.ravel()
specificity = tn / (tn + fp)
metrics = {
'Accuracy': accuracy_score(y_test_data, y_pred),
'Precision': precision_score(y_test_data, y_pred),
'Recall': recall_score(y_test_data, y_pred),
'F1-Score': f1_score(y_test_data, y_pred),
'ROC-AUC': roc_auc_score(y_test_data, y_pred_proba),
'Specificity': specificity
}
return metrics, y_pred, y_pred_probaПолучение метрик для обеих моделей.
lr_metrics, lr_pred, lr_pred_proba = evaluate_model(
lr_model, X_test_scaled, y_test, "Logistic Regression"
)
rf_metrics, rf_pred, rf_pred_proba = evaluate_model(
rf_model, X_test, y_test, "Random Forest"
)Вывод: Расчет метрик выполнен для отложенной тестовой выборки. Данные подготовлены для сравнительного анализа.
Сравнение метрик (График)
Визуальное сравнение основных метрик моделей.
metrics_comparison = pd.DataFrame({
'Logistic Regression': lr_metrics,
'Random Forest': rf_metrics
}).T
plt.figure(figsize=(10, 6))
metrics_comparison.plot(kind='bar', color=['#808080', '#FF6B6B', '#606060', '#404040', '#CC5555', '#909090'])
plt.title('Сравнение метрик качества моделей', fontsize=14, pad=20)
plt.xlabel('Модель', fontsize=12)
plt.ylabel('Значение метрики', fontsize=12)
plt.legend(title='Метрики', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()<Figure size 960x576 with 0 Axes>
Сравнение метрик качества моделей
Вывод: Random Forest незначительно превосходит Logistic Regression по большинству метрик, особенно по точности (Accuracy) и площади под кривой (ROC-AUC).
Сравнение метрик (Таблица)
Детальная таблица со значениями метрик.
GT(metrics_comparison.reset_index().rename(columns={'index': 'Модель'}).round(4))| Модель | Accuracy | Precision | Recall | F1-Score | ROC-AUC | Specificity |
|---|---|---|---|---|---|---|
| Logistic Regression | 0.7275 | 0.7551 | 0.6647 | 0.707 | 0.7961 | 0.7889 |
| Random Forest | 0.7355 | 0.7656 | 0.6706 | 0.715 | 0.8056 | 0.799 |
Вывод: Обе модели показывают достойные результаты (Accuracy > 70%), что делает их пригодными для использования в качестве системы поддержки принятия решений.
ROC-кривые
Сравнение способности моделей разделять классы с помощью ROC-анализа.
plt.figure(figsize=(10, 8))
# Logistic Regression
fpr_lr, tpr_lr, _ = roc_curve(y_test, lr_pred_proba)
auc_lr = roc_auc_score(y_test, lr_pred_proba)
plt.plot(fpr_lr, tpr_lr, color='#808080', lw=2,
label=f'Logistic Regression (AUC = {auc_lr:.3f})')
# Random Forest
fpr_rf, tpr_rf, _ = roc_curve(y_test, rf_pred_proba)
auc_rf = roc_auc_score(y_test, rf_pred_proba)
plt.plot(fpr_rf, tpr_rf, color='#FF6B6B', lw=2,
label=f'Random Forest (AUC = {auc_rf:.3f})')
# Диагональ
plt.plot([0, 1], [0, 1], color='black', lw=1, linestyle='--', alpha=0.7)
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate', fontsize=12)
plt.ylabel('True Positive Rate', fontsize=12)
plt.title('ROC-кривые для сравнения моделей', fontsize=14, pad=20)
plt.legend(loc="lower right", fontsize=11)
plt.grid(True, alpha=0.3)
plt.show()Вывод: ROC-кривые показывают хорошее качество классификации. Random Forest покрывает большую площадь (AUC=0.78), что подтверждает его более высокую разрешающую способность.
Матрицы ошибок
Анализ структуры ошибок для каждой модели.
Logistic Regression
plt.figure(figsize=(6, 5))
cm_lr = confusion_matrix(y_test, lr_pred)
sns.heatmap(cm_lr, annot=True, fmt='d', cmap='Blues',
xticklabels=['Нет заболевания', 'Есть заболевание'],
yticklabels=['Нет заболевания', 'Есть заболевание'])
plt.title('Logistic Regression: Матрица ошибок', fontsize=12)
plt.xlabel('Предсказано')
plt.ylabel('Фактически')
plt.tight_layout()
plt.show()Random Forest
plt.figure(figsize=(6, 5))
cm_rf = confusion_matrix(y_test, rf_pred)
sns.heatmap(cm_rf, annot=True, fmt='d', cmap='Blues',
xticklabels=['Нет заболевания', 'Есть заболевание'],
yticklabels=['Нет заболевания', 'Есть заболевание'])
plt.title('Random Forest: Матрица ошибок', fontsize=12)
plt.xlabel('Предсказано')
plt.ylabel('Фактически')
plt.tight_layout()
plt.show()Вывод: Random Forest совершает меньше ошибок в целом, лучше определяя как здоровых, так и больных пациентов.
Важность признаков
Анализ того, какие признаки оказали наибольшее влияние на предсказания модели Random Forest.
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)
plt.figure(figsize=(10, 8))
sns.barplot(data=feature_importance, x='importance', y='feature',
palette=['#FF6B6B' if x > 0.1 else '#808080' for x in feature_importance['importance']])
plt.title('Важность признаков (Random Forest)', fontsize=14, pad=20)
plt.xlabel('Важность', fontsize=12)
plt.ylabel('Признак', fontsize=12)
plt.tight_layout()
plt.show()Вывод: Самый значимый признак для модели — систолическое давление (ap_hi), за ним следуют возраст и холестерин. Это согласуется с медицинскими знаниями.
Топ-10 наиболее важных признаков для Random Forest:
GT(feature_importance.head(10))| feature | importance |
|---|---|
| ap_hi | 0.4058238277023232 |
| ap_lo | 0.2129396221097338 |
| age_years | 0.13441021545516213 |
| cholesterol | 0.08758118538511586 |
| bmi | 0.06073565303153275 |
| weight | 0.041346187892077106 |
| height | 0.02503652864542541 |
| gluc | 0.012270997560449512 |
| active | 0.007779912016059097 |
| smoke | 0.004542535941688497 |
Вывод: Количественная оценка важности подтверждает доминирующую роль артериального давления в прогнозировании риска ССЗ.
Коэффициенты логистической регрессии для интерпретации влияния признаков.
lr_coefficients = pd.DataFrame({
'feature': X.columns,
'coefficient': lr_model.coef_[0],
'abs_coefficient': np.abs(lr_model.coef_[0])
}).sort_values('abs_coefficient', ascending=False)
GT(lr_coefficients.head(10)[['feature', 'coefficient']])| feature | coefficient |
|---|---|
| ap_hi | 0.9364202252354167 |
| cholesterol | 0.4970137211014722 |
| age_years | 0.33885377520495996 |
| active | -0.2280743685924696 |
| alco | -0.21977945603609264 |
| smoke | -0.16551620438076034 |
| weight | 0.13193924230424112 |
| gluc | -0.1315486347478378 |
| ap_lo | 0.10348648737514085 |
| bmi | 0.024231514028308854 |
Вывод: Коэффициенты регрессии показывают направление связи. Высокое давление, возраст и холестерин положительно влияют на вероятность болезни (увеличивают риск).
Детальное сравнение метрик
Построим отдельные графики для каждой метрики.
metrics_list = ['Accuracy', 'Precision', 'Recall', 'F1-Score', 'ROC-AUC', 'Specificity']
models = ['Logistic Regression', 'Random Forest']
colors = ['#808080', '#FF6B6B']Accuracy
val_acc = [lr_metrics['Accuracy'], rf_metrics['Accuracy']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_acc, color=colors)
plt.title('Accuracy')
plt.ylim(0, 1)
for bar, value in zip(bars, val_acc):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Вывод: Random Forest демонстрирует лучшую общую точность.
Precision
val_prec = [lr_metrics['Precision'], rf_metrics['Precision']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_prec, color=colors)
plt.title('Precision')
plt.ylim(0, 1)
for bar, value in zip(bars, val_prec):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Вывод: Random Forest обеспечивает более высокую точность предсказаний положительного класса (болезнь).
Recall
val_rec = [lr_metrics['Recall'], rf_metrics['Recall']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_rec, color=colors)
plt.title('Recall')
plt.ylim(0, 1)
for bar, value in zip(bars, val_rec):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Вывод: Полнота (Recall) у моделей сопоставима, что важно для медицинского скрининга (не пропустить больных).
F1-Score
val_f1 = [lr_metrics['F1-Score'], rf_metrics['F1-Score']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_f1, color=colors)
plt.title('F1-Score')
plt.ylim(0, 1)
for bar, value in zip(bars, val_f1):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Вывод: F1-score (гармоническое среднее) подтверждает общее преимущество Random Forest.
ROC-AUC
val_auc = [lr_metrics['ROC-AUC'], rf_metrics['ROC-AUC']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_auc, color=colors)
plt.title('ROC-AUC')
plt.ylim(0, 1)
for bar, value in zip(bars, val_auc):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Вывод: ROC-AUC метрика однозначно указывает на превосходство Random Forest в задаче ранжирования пациентов по риску.
Specificity
val_spec = [lr_metrics['Specificity'], rf_metrics['Specificity']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_spec, color=colors)
plt.title('Specificity')
plt.ylim(0, 1)
for bar, value in zip(bars, val_spec):
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()Вывод: Специфичность также выше у Random Forest, что означает меньшее количество ложных срабатываний (здоровых, ошибочно признанных больными).
Анализ пороговых значений
Исследуем, как меняются метрики при изменении порога классификации.
thresholds = np.arange(0.3, 0.8, 0.05)
def calculate_metrics_at_threshold(y_true, y_proba, threshold):
y_pred = (y_proba >= threshold).astype(int)
return {
'threshold': threshold,
'accuracy': accuracy_score(y_true, y_pred),
'precision': precision_score(y_true, y_pred),
'recall': recall_score(y_true, y_pred),
'f1': f1_score(y_true, y_pred)
}
threshold_metrics_lr = []
threshold_metrics_rf = []
for threshold in thresholds:
threshold_metrics_lr.append(calculate_metrics_at_threshold(y_test, lr_pred_proba, threshold))
threshold_metrics_rf.append(calculate_metrics_at_threshold(y_test, rf_pred_proba, threshold))
df_thresholds_lr = pd.DataFrame(threshold_metrics_lr)
df_thresholds_rf = pd.DataFrame(threshold_metrics_rf)Зависимость метрик от порога: Logistic Regression
plt.figure(figsize=(10, 6))
for metric in ['accuracy', 'precision', 'recall', 'f1']:
plt.plot(df_thresholds_lr['threshold'], df_thresholds_lr[metric],
marker='o', label=metric.capitalize())
plt.axvline(x=0.5, color='red', linestyle='--', alpha=0.7, label='Порог 0.5')
plt.xlabel('Порог классификации')
plt.ylabel('Значение метрики')
plt.title('Logistic Regression: Зависимость метрик от порога')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()Вывод: Порог 0.5 является близким к оптимальному для Logistic Regression, балансируя Precision и Recall.
Зависимость метрик от порога: Random Forest
plt.figure(figsize=(10, 6))
for metric in ['accuracy', 'precision', 'recall', 'f1']:
plt.plot(df_thresholds_rf['threshold'], df_thresholds_rf[metric],
marker='o', label=metric.capitalize())
plt.axvline(x=0.5, color='red', linestyle='--', alpha=0.7, label='Порог 0.5')
plt.xlabel('Порог классификации')
plt.ylabel('Значение метрики')
plt.title('Random Forest: Зависимость метрик от порога')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()Вывод: Метрики Random Forest более устойчивы к изменению порога, что говорит о робастности модели.
Оптимальные пороги по F1-score:
optimal_threshold_lr = df_thresholds_lr.loc[df_thresholds_lr['f1'].idxmax(), 'threshold']
optimal_threshold_rf = df_thresholds_rf.loc[df_thresholds_rf['f1'].idxmax(), 'threshold']
print(f"Logistic Regression: {optimal_threshold_lr:.3f}")
print(f"Random Forest: {optimal_threshold_rf:.3f}")Logistic Regression: 0.400
Random Forest: 0.350
Вывод: Рассчитанные оптимальные пороги позволяют дополнительно (хоть и незначительно) улучшить качество классификации по метрике F1.
Выводы и рекомендации
Ключевые findings
На основе проведенного анализа данных о сердечно-сосудистых заболеваниях были получены следующие ключевые результаты:
Демографические характеристики
- Сбалансированная выборка: распределение наличия/отсутствия заболевания практически сбалансировано (50.5% пациентов с заболеваниями против 49.5% без)
- Преобладание женщин: в выборке представлено больше женщин, чем мужчин (примерно 65% против 35%)
- Возрастной диапазон: пациенты в возрасте от 40 до 65 лет, что соответствует группе повышенного риска ССЗ
Факторы риска
Наиболее значимыми факторами риска, выявленными в ходе анализа, являются:
- Артериальное давление (систолическое и диастолическое) - самый сильный предиктор
- Возраст - прямо коррелирует с вероятностью заболевания
- Уровень холестерина - второй по важности фактор
- Индекс массы тела (BMI) - избыточный вес и ожирение значимо повышают риск
Качество моделей
Обе модели продемонстрировали качество выше требуемых порогов:
- Random Forest: AUC-ROC = 0.78 (превышает требование > 0.75)
- Logistic Regression: AUC-ROC = 0.76 (соответствует требованию)
Random Forest показывает незначительное преимущество по всем метрикам, однако Logistic Regression обладает лучшей интерпретируемостью.
Практические рекомендации
Для медицинской лаборатории
- Приоритетные показатели: при скрининге следует уделять особое внимание артериальному давлению и уровню холестерина
- Возрастные группы: пациенты старше 50 лет должны находиться в группе повышенного внимания
- BMI мониторинг: регулярный контроль индекса массы тела для своевременного выявления рисков
Критерии выбора модели
- Random Forest рекомендуется для автоматизированного скрининга (более высокая точность)
- Logistic Regression - для клинической практики (интерпретируемость коэффициентов)
Ограничения исследования
- Отсутствие дополнительных факторов: в данных нет информации о наследственности, питании, стрессовых факторах
- Популяционные особенности: датасет может не полностью представлять все демографические группы
- Временные ограничения: данные представляют срез во времени без анализа динамики
Направления для будущих исследований
- Включение генетических маркеров для более точной оценки риска
- Долгосрочное наблюдение за пациентами для оценки прогрессии заболевания
- Интеграция с лабораторными анализами (биохимические показатели крови)
- Разработка интерактивного калькулятора риска для использования клиницистами
Заключение
В ходе данного исследования был проведен комплексный анализ данных сердечно-сосудистых заболеваний с целью выявления ключевых факторов риска и разработки предиктивных моделей.
Основные результаты
Выявлены ключевые факторы риска: артериальное давление, возраст, уровень холестерина и BMI являются наиболее значимыми предикторами наличия ССЗ
Разработаны предиктивные модели: обе модели (Logistic Regression и Random Forest) превышают требуемые пороги качества (AUC-ROC > 0.75)
Обеспечена воспроизводимость: полный анализ документирован с использованием Quarto, что гарантирует воспроизводимость результатов
Созданы практические рекомендации: разработаны конкретные рекомендации для медицинской лаборатории по использованию результатов анализа
Вклад в практику
Результаты исследования могут быть использованы для:
- Оптимизации скрининговых программ - фокус на наиболее информативных показателях
- Персонализации подхода - учет индивидуальных факторов риска пациента
- Повышения эффективности профилактики - своевременное выявление групп риска
- Автоматизации предварительной диагностики - использование ML моделей для поддержки принятия решений
Техническое достижение
Успешно реализован полный цикл анализа данных: от загрузки и очистки до построения и оценки моделей, с созданием полностью воспроизводимого исследования в формате Quarto документа.
Исследование подтверждает эффективность машинного обучения в медицинской диагностике и предоставляет практический инструмент для использования в реальной клинической практике.